1 /**
2 Copyright: Copyright (c) 2016-2017, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 
10 Utility functions for Clang Compilation Databases.
11 */
12 module code_checker.compile_db;
13 
14 import std.json : JSONValue;
15 import std.typecons : Nullable;
16 import logger = std.experimental.logger;
17 import std.exception : collectException;
18 
19 import code_checker.types : AbsolutePath;
20 
21 public import code_checker.compile_db.user_filerange;
22 
23 version (unittest) {
24     import std.path : buildPath;
25     import unit_threaded : Name, shouldEqual;
26 }
27 
28 @safe:
29 
30 /** Hold an entry from the compilation database.
31  *
32  * The following information is from the official specification.
33  * $(LINK2 http://clang.llvm.org/docs/JSONCompilationDatabase.html, Standard)
34  *
35  * directory: The working directory of the compilation. All paths specified in
36  * the command or file fields must be either absolute or relative to this
37  * directory.
38  *
39  * file: The main translation unit source processed by this compilation step.
40  * This is used by tools as the key into the compilation database. There can be
41  * multiple command objects for the same file, for example if the same source
42  * file is compiled with different configurations.
43  *
44  * command: The compile command executed. After JSON unescaping, this must be a
45  * valid command to rerun the exact compilation step for the translation unit
46  * in the environment the build system uses. Parameters use shell quoting and
47  * shell escaping of quotes, with ‘"‘ and ‘\‘ being the only special
48  * characters. Shell expansion is not supported.
49  *
50  * argumets: The compile command executed as list of strings. Either arguments
51  * or command is required.
52  *
53  * output: The name of the output created by this compilation step. This field
54  * is optional. It can be used to distinguish different processing modes of the
55  * same input file.
56  *
57  * Dextool additions.
58  * The standard do not specify how to treat "directory" when it is a relative
59  * path. The logic chosen in dextool is to treat it as relative to the path
60  * the compilation database file is read from.
61  */
62 @safe struct CompileCommand {
63     import code_checker.types : DirName;
64 
65     static import code_checker.types;
66 
67     /// The raw filename from the tuples "file" value.
68     alias FileName = code_checker.types.FileName;
69 
70     /// The combination of the tuples "file" and "directory" value.
71     static struct AbsoluteFileName {
72         code_checker.types.AbsoluteFileName payload;
73         alias payload this;
74 
75         this(AbsoluteDirectory work_dir, string raw_path) {
76             payload = AbsolutePath(FileName(raw_path), DirName(work_dir));
77         }
78     }
79 
80     /// The tuples "directory" value converted to the absolute path.
81     static struct AbsoluteDirectory {
82         code_checker.types.AbsoluteDirectory payload;
83         alias payload this;
84 
85         this(AbsoluteCompileDbDirectory db_path, string raw_path) {
86             payload = AbsolutePath(FileName(raw_path), DirName(db_path));
87         }
88     }
89 
90     /// The raw command from the tuples "command" value.
91     static struct Command {
92         string[] payload;
93         alias payload this;
94         bool hasValue() @safe pure nothrow const @nogc {
95             return payload.length != 0;
96         }
97     }
98 
99     /// The raw arguments from the tuples "arguments" value.
100     static struct Arguments {
101         string[] payload;
102         alias payload this;
103         bool hasValue() @safe pure nothrow const @nogc {
104             return payload.length != 0;
105         }
106     }
107 
108     /// The path to the output from running the command
109     static struct Output {
110         string payload;
111         alias payload this;
112         bool hasValue() @safe pure nothrow const @nogc {
113             return payload.length != 0;
114         }
115     }
116 
117     ///
118     FileName file;
119     ///
120     AbsoluteFileName absoluteFile;
121     ///
122     AbsoluteDirectory directory;
123     ///
124     Command command;
125     ///
126     Arguments arguments;
127     ///
128     Output output;
129     ///
130     AbsoluteFileName absoluteOutput;
131 }
132 
133 /// The path to the compilation database.
134 struct CompileDbFile {
135     string payload;
136     alias payload this;
137 }
138 
139 /// The absolute path to the directory the compilation database reside at.
140 struct AbsoluteCompileDbDirectory {
141     string payload;
142     alias payload this;
143 
144     invariant {
145         import std.path : isAbsolute;
146 
147         assert(payload.isAbsolute);
148     }
149 
150     this(string file_path) {
151         import std.path : buildNormalizedPath, dirName, absolutePath;
152 
153         payload = buildNormalizedPath(file_path).absolutePath.dirName;
154     }
155 
156     this(CompileDbFile db) {
157         this(cast(string) db);
158     }
159 
160     unittest {
161         import std.path;
162 
163         auto dir = AbsoluteCompileDbDirectory(".");
164         assert(dir.isAbsolute);
165     }
166 }
167 
168 /// A complete compilation database.
169 struct CompileCommandDB {
170     CompileCommand[] payload;
171     alias payload this;
172 }
173 
174 // The result of searching for a file in a compilation DB.
175 // The file may be occur more than one time therefor an array.
176 struct CompileCommandSearch {
177     CompileCommand[] payload;
178     alias payload this;
179 }
180 
181 /**
182  * Trusted: opIndex for JSONValue is @safe in DMD-2.077.0
183  * remove the trusted attribute when the minimal requirement is upgraded.
184  */
185 private Nullable!CompileCommand toCompileCommand(JSONValue v, AbsoluteCompileDbDirectory db_dir) nothrow @trusted {
186     import std.algorithm : map, filter, joiner, splitter;
187     import std.array : array;
188     import std.exception : assumeUnique;
189     import std.json : JSON_TYPE;
190     import std.range : only;
191     import std.utf : byUTF;
192 
193     string[] command;
194     try {
195         command = v["command"].str.splitter.filter!(a => a.length != 0).array;
196     } catch (Exception ex) {
197     }
198 
199     string[] arguments;
200     try {
201         enum j_arg = "arguments";
202         const auto j_type = v[j_arg].type;
203         if (j_type == JSON_TYPE.STRING)
204             arguments = v[j_arg].str.splitter.filter!(a => a.length != 0).array;
205         else if (j_type == JSON_TYPE.ARRAY) {
206             import std.range;
207 
208             // TODO unnecessary to join it
209             arguments = v[j_arg].arrayNoRef.filter!(a => a.type == JSON_TYPE.STRING)
210                 .map!(a => a.str).filter!(a => a.length != 0).array;
211         }
212     } catch (Exception ex) {
213     }
214 
215     if (command.length == 0 && arguments.length == 0) {
216         logger.error("Unable to parse json tuple, both command and arguments are empty")
217             .collectException;
218         return typeof(return)();
219     }
220 
221     string output;
222     try {
223         output = v["output"].str;
224     } catch (Exception ex) {
225     }
226 
227     try {
228         const directory = v["directory"];
229         const file = v["file"];
230 
231         foreach (a; only(directory, file).map!(a => !a.isNull
232                 && a.type == JSON_TYPE.STRING).filter!(a => !a)) {
233             // sanity check.
234             // if any element is false then break early.
235             return typeof(return)();
236         }
237 
238         return toCompileCommand(directory.str, file.str, command, db_dir, arguments, output);
239     } catch (Exception ex) {
240         logger.error("Unable to parse json: " ~ ex.msg).collectException;
241     }
242 
243     return typeof(return)();
244 }
245 
246 /** Transform a json entry to a CompileCommand.
247  *
248  * This function is under no circumstances meant to be exposed outside this module.
249  * The API is badly designed for common use because it relies on the position
250  * order of the strings for their meaning.
251  */
252 private Nullable!CompileCommand toCompileCommand(string directory, string file,
253         string[] command, AbsoluteCompileDbDirectory db_dir, string[] arguments, string output) nothrow {
254     // expects that v is a tuple of 3 json values with the keys directory,
255     // command, file
256 
257     Nullable!CompileCommand rval;
258 
259     try {
260         auto abs_workdir = CompileCommand.AbsoluteDirectory(db_dir, directory);
261         auto abs_file = CompileCommand.AbsoluteFileName(abs_workdir, file);
262         auto abs_output = CompileCommand.AbsoluteFileName(abs_workdir, output);
263         // dfmt off
264         rval = CompileCommand(
265             CompileCommand.FileName(file),
266             abs_file,
267             abs_workdir,
268             CompileCommand.Command(command),
269             CompileCommand.Arguments(arguments),
270             CompileCommand.Output(output),
271             abs_output);
272         // dfmt on
273     } catch (Exception ex) {
274         logger.error("Unable to parse json: " ~ ex.msg).collectException;
275     }
276 
277     return rval;
278 }
279 
280 /** Parse a CompilationDatabase.
281  *
282  * Params:
283  *  raw_input = the content of the CompilationDatabase.
284  *  in_file = path to the compilation database file.
285  *  out_range = range to write the output to.
286  */
287 private void parseCommands(T)(string raw_input, CompileDbFile in_file, ref T out_range) nothrow {
288     import std.json : parseJSON, JSONException;
289 
290     static void put(T)(JSONValue v, AbsoluteCompileDbDirectory dbdir, ref T out_range) nothrow {
291         import std.algorithm : map, filter;
292         import std.array : array;
293 
294         try {
295             // dfmt off
296             foreach (e; v.array()
297                      // map the JSON tuples to D structs
298                      .map!(a => toCompileCommand(a, dbdir))
299                      // remove invalid
300                      .filter!(a => !a.isNull)
301                      .map!(a => a.get)) {
302                 out_range.put(e);
303             }
304             // dfmt on
305         } catch (Exception ex) {
306             logger.error("Unable to parse json:" ~ ex.msg).collectException;
307         }
308     }
309 
310     try {
311         // trusted: is@safe in DMD-2.077.0
312         // remove the trusted attribute when the minimal requirement is upgraded.
313         auto json = () @trusted{ return parseJSON(raw_input); }();
314         auto as_dir = AbsoluteCompileDbDirectory(in_file);
315 
316         // trusted: this function is private so the only user of it is this module.
317         // the only problem would be in the out_range. It is assumed that the
318         // out_range takes care of the validation and other security aspects.
319         () @trusted{ put(json, as_dir, out_range); }();
320     } catch (Exception ex) {
321         logger.error("Error while parsing compilation database: " ~ ex.msg).collectException;
322     }
323 }
324 
325 void fromFile(T)(CompileDbFile filename, ref T app) {
326     import std.algorithm : joiner;
327     import std.conv : text;
328     import std.stdio : File;
329 
330     // trusted: using the GC for memory management.
331     // assuming any UTF-8 errors in the input is validated by phobos byLineCopy.
332     auto raw = () @trusted{
333         return File(cast(string) filename).byLineCopy.joiner.text;
334     }();
335 
336     raw.parseCommands(filename, app);
337 }
338 
339 void fromFiles(T)(CompileDbFile[] fnames, ref T app) {
340     import std.file : exists;
341 
342     foreach (f; fnames) {
343         if (!exists(f))
344             throw new Exception("File do not exist: " ~ f);
345         f.fromFile(app);
346     }
347 }
348 
349 /** Return default path if argument is null.
350  */
351 CompileDbFile[] orDefaultDb(string[] cli_path) @safe pure nothrow {
352     import std.array : array;
353     import std.algorithm : map;
354 
355     if (cli_path.length == 0) {
356         return [CompileDbFile("compile_commands.json")];
357     }
358 
359     return cli_path.map!(a => CompileDbFile(a)).array();
360 }
361 
362 /** Contains the results of a search in the compilation database.
363  *
364  * When searching for the compile command for a file, the compilation db can
365  * return several commands, as the file may have been compiled with different
366  * options in different parts of the project.
367  *
368  * Params:
369  *  glob = glob pattern to find a matching file in the DB against
370  */
371 CompileCommandSearch find(CompileCommandDB db, string glob) @safe
372 in {
373     debug logger.trace("Looking for " ~ glob);
374 }
375 out (result) {
376     import std.conv : to;
377 
378     debug logger.trace("Found " ~ to!string(result));
379 }
380 body {
381     import std.path : globMatch;
382 
383     foreach (a; db) {
384         if (a.absoluteFile == glob)
385             return CompileCommandSearch([a]);
386         else if (a.file == glob)
387             return CompileCommandSearch([a]);
388         else if (globMatch(a.absoluteFile, glob))
389             return CompileCommandSearch([a]);
390         else if (a.absoluteOutput == glob)
391             return CompileCommandSearch([a]);
392         else if (a.output == glob)
393             return CompileCommandSearch([a]);
394         else if (globMatch(a.absoluteOutput, glob))
395             return CompileCommandSearch([a]);
396     }
397 
398     logger.errorf("\n%s\nNo match found in the compile command database", db.toString);
399 
400     return CompileCommandSearch();
401 }
402 
403 struct SearchResult {
404     string[] cflags;
405     AbsolutePath absoluteFile;
406 }
407 
408 /** Append the compiler flags if a match is found in the DB or error out.
409  */
410 Nullable!(SearchResult) appendOrError(CompileCommandDB compile_db,
411         const string[] cflags, const string input_file) @safe {
412 
413     return appendOrError(compile_db, cflags, input_file, defaultCompilerFilter);
414 }
415 
416 /** Append the compiler flags if a match is found in the DB or error out.
417  */
418 Nullable!(SearchResult) appendOrError(CompileCommandDB compile_db,
419         const string[] cflags, const string input_file, const CompileCommandFilter flag_filter) @safe {
420     auto compile_commands = compile_db.find(input_file.idup);
421     debug {
422         logger.trace(compile_commands.length > 0,
423                 "CompilationDatabase match (by filename):\n", compile_commands.toString);
424         if (compile_commands.length == 0) {
425             logger.trace(compile_db.toString);
426         }
427 
428         logger.tracef("CompilationDatabase filter: %s", flag_filter);
429     }
430 
431     typeof(return) rval;
432     if (compile_commands.length == 0) {
433         logger.warning("File not found in compilation database: ", input_file);
434         return rval;
435     } else {
436         rval = SearchResult.init;
437         rval.cflags = cflags ~ compile_commands[0].parseFlag(flag_filter);
438         rval.absoluteFile = compile_commands[0].absoluteFile;
439     }
440 
441     return rval;
442 }
443 
444 string toString(CompileCommand[] db) @safe pure {
445     import std.array;
446     import std.algorithm : map, joiner;
447     import std.conv : text;
448     import std.format : formattedWrite;
449 
450     auto app = appender!string();
451 
452     foreach (a; db) {
453         formattedWrite(app, "%s\n  %s\n  %s\n", a.directory, a.file, a.absoluteFile);
454 
455         if (a.output.hasValue) {
456             formattedWrite(app, "  %s\n", a.output);
457             formattedWrite(app, "  %s\n", a.absoluteOutput);
458         }
459 
460         if (a.command.hasValue)
461             formattedWrite(app, "  %-(%s %)\n", a.command);
462 
463         if (a.arguments.hasValue)
464             formattedWrite(app, "  %-(%s %)\n", a.arguments);
465     }
466 
467     return app.data;
468 }
469 
470 string toString(CompileCommandDB db) @safe pure {
471     return toString(db.payload);
472 }
473 
474 string toString(CompileCommandSearch search) @safe pure {
475     return toString(search.payload);
476 }
477 
478 const auto defaultCompilerFilter = CompileCommandFilter(defaultCompilerFlagFilter, 1);
479 
480 /// Returns: array of default flags to exclude.
481 auto defaultCompilerFlagFilter() @safe {
482     import std.array : appender;
483 
484     auto app = appender!(FilterClangFlag[])();
485 
486     // dfmt off
487     foreach (f; [
488              // remove basic compile flag irrelevant for AST generation
489              "-c", "-o",
490              // machine dependent flags
491              "-m",
492              // machine dependent flags, AVR
493              "-nodevicelib", "-Waddr-space-convert",
494              // machine dependent flags, VxWorks
495              "-non-static", "-Bstatic", "-Bdynamic", "-Xbind-lazy", "-Xbind-now",
496              // blacklist all -f because most aren not compatible with clang
497              "-f",
498              // linker flags, irrelevant for the AST
499              "-static", "-shared", "-rdynamic", "-s", "-l", "-L", "-z", "-u", "-T", "-Xlinker",
500              // a linker flag with filename as one argument
501              "-l",
502              // remove some of the preprocessor flags, irrelevant for the AST
503              "-MT", "-MF", "-MD", "-MQ", "-MMD", "-MP", "-MG", "-E", "-cc1", "-S", "-M", "-MM", "-###",
504              ]) {
505         app.put(FilterClangFlag(f));
506     }
507     // dfmt on
508 
509     return app.data;
510 }
511 
512 struct CompileCommandFilter {
513     FilterClangFlag[] filter;
514     int skipCompilerArgs = 1;
515 }
516 
517 /// Parsed compiler flags.
518 struct ParseFlags {
519     /// The includes used in the compile command
520     static struct Includes {
521         string[] payload;
522         alias payload this;
523     }
524 
525     ///
526     Includes includes;
527 
528     string[] flags;
529     alias flags this;
530 }
531 
532 /** Filter and normalize the compiler flags.
533  *
534  *  - Sanitize the compiler command by removing flags matching the filter.
535  *  - Remove excess white space.
536  *  - Convert all filenames to absolute path.
537  */
538 ParseFlags parseFlag(const CompileCommand cmd, const CompileCommandFilter flag_filter) @safe {
539     import std.algorithm : among;
540 
541     static bool excludeStartWith(const string raw_flag, const FilterClangFlag[] flag_filter) @safe {
542         import std.algorithm : startsWith, filter, count;
543         import std.array : split, empty;
544 
545         // the purpuse is to find if any of the flags in flag_filter matches
546         // the start of flag.
547 
548         bool delegate(const FilterClangFlag) @safe cmp;
549 
550         const parts = raw_flag.split('=');
551         if (parts.length == 2) {
552             // is a -foo=bar flag thus exact match is the only sensible
553             cmp = (const FilterClangFlag a) => parts[0] == a.payload;
554         } else {
555             // the flag has the argument merged thus have to check if the start match
556             cmp = (const FilterClangFlag a) => raw_flag.startsWith(a.payload);
557         }
558 
559         // dfmt off
560         return 0 != flag_filter
561             .filter!(a => a.kind == FilterClangFlag.Kind.exclude)
562             // keep flags that are at least the length of values
563             .filter!(a => raw_flag.length >= a.length)
564             // if the flag is any of those in filter
565             .filter!cmp
566             .count();
567         // dfmt on
568     }
569 
570     static bool isCombinedIncludeFlag(string flag) @safe {
571         // if an include flag make it absolute, as one argument by checking
572         // length. 3 is to only match those that are -Ixyz
573         return flag.length >= 3 && flag[0 .. 2] == "-I";
574     }
575 
576     static bool isNotAFlag(string flag) @safe {
577         // good enough if it seem to be a file
578         return flag.length >= 1 && flag[0] != '-';
579     }
580 
581     /// Flags that take an argument that is a path that need to be transformed
582     /// to an absolute path.
583     static bool isFlagAndPath(string flag) @safe {
584         // list derived from clang --help
585         return 0 != flag.among("-I", "-idirafter", "-iframework", "-imacros",
586                 "-include-pch", "-include", "-iquote", "-isysroot", "-isystem-after", "-isystem");
587     }
588 
589     /// Flags that take an argument that is NOT a path.
590     static bool isFlagAndValue(string flag) @safe {
591         return 0 != flag.among("-D");
592     }
593 
594     static ParseFlags filterPair(T)(ref T r, CompileCommand.AbsoluteDirectory workdir,
595             const FilterClangFlag[] flag_filter, bool keepFirstArg) @safe {
596         enum State {
597             /// first argument is kept even though it isn't a flag because it is the command
598             firstArg,
599             /// keep the next flag IF none of the other transitions happens
600             keep,
601             /// forcefully keep the next argument as raw data
602             priorityKeepNextArg,
603             /// keep the next argument and transform to an absolute path
604             pathArgumentToAbsolute,
605             /// skip the next arg
606             skip,
607             /// skip the next arg, if it is not a flag
608             skipIfNotFlag,
609         }
610 
611         import std.path : buildNormalizedPath, absolutePath;
612         import std.array : appender;
613         import std.range : ElementType;
614 
615         auto st = keepFirstArg ? State.firstArg : State.keep;
616         auto rval = appender!(string[]);
617         auto includes = appender!(string[]);
618 
619         foreach (arg; r) {
620             // First states and how to handle those.
621             // Then transitions from the state keep, which is the default state.
622             //
623             // The user controlled excludeStartWith must be before any other
624             // conditions after the states. It is to give the user the ability
625             // to filter out any flag.
626 
627             if (st == State.firstArg) {
628                 // keep it, it is the command
629                 rval.put(arg);
630                 st = State.keep;
631             } else if (st == State.skip) {
632                 st = State.keep;
633             } else if (st == State.skipIfNotFlag && isNotAFlag(arg)) {
634                 st = State.keep;
635             } else if (st == State.pathArgumentToAbsolute) {
636                 st = State.keep;
637                 auto p = buildNormalizedPath(workdir, arg).absolutePath;
638                 rval.put(p);
639                 includes.put(p);
640             } else if (st == State.priorityKeepNextArg) {
641                 st = State.keep;
642                 rval.put(arg);
643             } else if (excludeStartWith(arg, flag_filter)) {
644                 st = State.skipIfNotFlag;
645             } else if (isCombinedIncludeFlag(arg)) {
646                 rval.put("-I");
647                 auto p = buildNormalizedPath(workdir, arg[2 .. $]).absolutePath;
648                 rval.put(p);
649                 includes.put(p);
650             } else if (isFlagAndPath(arg)) {
651                 rval.put(arg);
652                 st = State.pathArgumentToAbsolute;
653             } else if (isFlagAndValue(arg)) {
654                 rval.put(arg);
655                 st = State.priorityKeepNextArg;
656             }  // parameter that seem to be filenames, remove
657             else if (isNotAFlag(arg)) {
658                 // skipping
659             } else {
660                 rval.put(arg);
661             }
662         }
663 
664         return ParseFlags(ParseFlags.Includes(includes.data), rval.data);
665     }
666 
667     import std.algorithm : filter, splitter, min;
668 
669     string[] pass1 = () @safe{
670         // If `arguments` is used then it is already _perfect_.
671         if (cmd.arguments.hasValue)
672             return cmd.arguments.payload;
673         if (flag_filter.skipCompilerArgs != 0)
674             return cmd.command.payload;
675         // skip parameters matching the filter IF `command` where used.
676         return cmd.command[min(flag_filter.skipCompilerArgs, cmd.command.length) .. $];
677     }().dup;
678 
679     // `arguments` in a compilation database do not have the compiler binary in
680     // the string thus skipCompilerArgs isn't needed.
681     // This is different from the case where skipCompilerArgs is zero, which is
682     // intended to force filterPair that the first value in the range is the
683     // compiler, not a filename, and shall be kept.
684     bool keep_first_arg = !cmd.arguments.hasValue && flag_filter.skipCompilerArgs == 0;
685 
686     return filterPair(pass1, cmd.directory, flag_filter.filter, keep_first_arg);
687 }
688 
689 /// Import and merge many compilation databases into one DB.
690 CompileCommandDB fromArgCompileDb(string[] paths) @safe {
691     import std.array : appender;
692 
693     auto app = appender!(CompileCommand[])();
694     paths.orDefaultDb.fromFiles(app);
695 
696     return CompileCommandDB(app.data);
697 }
698 
699 /// Flags to exclude from the flags passed on to the clang parser.
700 struct FilterClangFlag {
701     string payload;
702     alias payload this;
703 
704     enum Kind {
705         exclude
706     }
707 
708     Kind kind;
709 }
710 
711 @("Should be cflags with all unnecessary flags removed")
712 unittest {
713     auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-MD", "-lfoo.a", "-l", "bar.a", "-I",
714             "bar", "-Igun", "-c", "a_filename.c"], AbsoluteCompileDbDirectory("/home"), null, null);
715     auto s = cmd.parseFlag(defaultCompilerFilter);
716     s.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]);
717     s.includes.shouldEqual(["/home/bar", "/home/gun"]);
718 }
719 
720 @("Should be cflags with some excess spacing")
721 unittest {
722     auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-MD", "-lfoo.a", "-l",
723             "bar.a", "-I", "bar", "-Igun"], AbsoluteCompileDbDirectory("/home"), null, null);
724 
725     auto s = cmd.parseFlag(defaultCompilerFilter);
726     s.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]);
727     s.includes.shouldEqual(["/home/bar", "/home/gun"]);
728 }
729 
730 @("Should be cflags with machine dependent removed")
731 unittest {
732     auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-mfoo", "-m", "bar",
733             "-MD", "-lfoo.a", "-l", "bar.a", "-I", "bar", "-Igun", "-c", "a_filename.c"],
734             AbsoluteCompileDbDirectory("/home"), null, null);
735 
736     auto s = cmd.parseFlag(defaultCompilerFilter);
737     s.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]);
738     s.includes.shouldEqual(["/home/bar", "/home/gun"]);
739 }
740 
741 @("Should be cflags with all -f removed")
742 unittest {
743     auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-fmany-fooo", "-I", "bar", "-fno-fooo", "-Igun",
744             "-flolol", "-c", "a_filename.c"], AbsoluteCompileDbDirectory("/home"), null, null);
745 
746     auto s = cmd.parseFlag(defaultCompilerFilter);
747     s.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]);
748     s.includes.shouldEqual(["/home/bar", "/home/gun"]);
749 }
750 
751 @("shall NOT remove -std=xyz flags")
752 unittest {
753     auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-std=c++11",
754             "-c", "a_filename.c"], AbsoluteCompileDbDirectory("/home"), null, null);
755 
756     auto s = cmd.parseFlag(defaultCompilerFilter);
757     s.shouldEqual(["-std=c++11"]);
758 }
759 
760 @("Shall keep all compiler flags as they are")
761 unittest {
762     auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-Da", "-D",
763             "b"], AbsoluteCompileDbDirectory("/home"), null, null);
764 
765     auto s = cmd.parseFlag(defaultCompilerFilter);
766     s.shouldEqual(["-Da", "-D", "b"]);
767 }
768 
769 version (unittest) {
770     import std.file : getcwd;
771     import std.path : absolutePath;
772     import std.format : format;
773 
774     // contains a bit of extra junk that is expected to be removed
775     immutable string dummy_path = "/path/to/../to/./db/compilation_db.json";
776     immutable string dummy_dir = "/path/to/db";
777 
778     enum raw_dummy1 = `[
779     {
780         "directory": "dir1/dir2",
781         "command": "g++ -Idir1 -c -o binary file1.cpp",
782         "file": "file1.cpp"
783     }
784 ]`;
785 
786     enum raw_dummy2 = `[
787     {
788         "directory": "dir",
789         "command": "g++ -Idir1 -c -o binary file1.cpp",
790         "file": "file1.cpp"
791     },
792     {
793         "directory": "dir",
794         "command": "g++ -Idir1 -c -o binary file2.cpp",
795         "file": "file2.cpp"
796     }
797 ]`;
798 
799     enum raw_dummy3 = `[
800     {
801         "directory": "dir1",
802         "command": "g++ -Idir1 -c -o binary file3.cpp",
803         "file": "file3.cpp"
804     },
805     {
806         "directory": "dir2",
807         "command": "g++ -Idir1 -c -o binary file3.cpp",
808         "file": "file3.cpp"
809     }
810 ]`;
811 
812     enum raw_dummy4 = `[
813     {
814         "directory": "dir1",
815         "arguments": "-Idir1 -c -o binary file3.cpp",
816         "file": "file3.cpp",
817         "output": "file3.o"
818     },
819     {
820         "directory": "dir2",
821         "arguments": "-Idir1 -c -o binary file3.cpp",
822         "file": "file3.cpp",
823         "output": "file3.o"
824     }
825 ]`;
826 
827     enum raw_dummy5 = `[
828     {
829         "directory": "dir1",
830         "arguments": ["-Idir1", "-c", "-o", "binary", "file3.cpp"],
831         "file": "file3.cpp",
832         "output": "file3.o"
833     },
834     {
835         "directory": "dir2",
836         "arguments": ["-Idir1", "-c", "-o", "binary", "file3.cpp"],
837         "file": "file3.cpp",
838         "output": "file3.o"
839     }
840 ]`;
841 }
842 
843 version (unittest) {
844     import std.array : appender;
845     import unit_threaded : writelnUt;
846 }
847 
848 @("Should be a compile command DB")
849 unittest {
850     auto app = appender!(CompileCommand[])();
851     raw_dummy1.parseCommands(CompileDbFile(dummy_path), app);
852     auto cmds = app.data;
853 
854     assert(cmds.length == 1);
855     cmds[0].directory.shouldEqual(dummy_dir ~ "/dir1/dir2");
856     cmds[0].command.shouldEqual(["g++", "-Idir1", "-c", "-o", "binary", "file1.cpp"]);
857     cmds[0].file.shouldEqual("file1.cpp");
858     cmds[0].absoluteFile.shouldEqual(dummy_dir ~ "/dir1/dir2/file1.cpp");
859 }
860 
861 @("Should be a DB with two entries")
862 unittest {
863     auto app = appender!(CompileCommand[])();
864     raw_dummy2.parseCommands(CompileDbFile(dummy_path), app);
865     auto cmds = app.data;
866 
867     cmds[0].file.shouldEqual("file1.cpp");
868     cmds[1].file.shouldEqual("file2.cpp");
869 }
870 
871 @("Should find filename")
872 unittest {
873     auto app = appender!(CompileCommand[])();
874     raw_dummy2.parseCommands(CompileDbFile(dummy_path), app);
875     auto cmds = CompileCommandDB(app.data);
876 
877     auto found = cmds.find(dummy_dir ~ "/dir/file2.cpp");
878     assert(found.length == 1);
879     found[0].file.shouldEqual("file2.cpp");
880 }
881 
882 @("Should find no match by using an absolute path that doesn't exist in DB")
883 unittest {
884     auto app = appender!(CompileCommand[])();
885     raw_dummy2.parseCommands(CompileDbFile(dummy_path), app);
886     auto cmds = CompileCommandDB(app.data);
887 
888     auto found = cmds.find("./file2.cpp");
889     assert(found.length == 0);
890 }
891 
892 @("Should find one match by using the absolute filename to disambiguous")
893 unittest {
894     import unit_threaded : writelnUt;
895 
896     auto app = appender!(CompileCommand[])();
897     raw_dummy3.parseCommands(CompileDbFile(dummy_path), app);
898     auto cmds = CompileCommandDB(app.data);
899 
900     auto found = cmds.find(dummy_dir ~ "/dir2/file3.cpp");
901     assert(found.length == 1);
902 
903     found.toString.shouldEqual(format("%s/dir2
904   file3.cpp
905   %s/dir2/file3.cpp
906   g++ -Idir1 -c -o binary file3.cpp
907 ", dummy_dir, dummy_dir));
908 }
909 
910 @("Should be a pretty printed search result")
911 unittest {
912     auto app = appender!(CompileCommand[])();
913     raw_dummy2.parseCommands(CompileDbFile(dummy_path), app);
914     auto cmds = CompileCommandDB(app.data);
915     auto found = cmds.find(dummy_dir ~ "/dir/file2.cpp");
916 
917     found.toString.shouldEqual(format("%s/dir
918   file2.cpp
919   %s/dir/file2.cpp
920   g++ -Idir1 -c -o binary file2.cpp
921 ", dummy_dir, dummy_dir));
922 }
923 
924 @("Should be a compile command DB with relative path")
925 unittest {
926     enum raw = `[
927     {
928         "directory": ".",
929         "command": "g++ -Idir1 -c -o binary file1.cpp",
930         "file": "file1.cpp"
931     }
932     ]`;
933     auto app = appender!(CompileCommand[])();
934     raw.parseCommands(CompileDbFile(dummy_path), app);
935     auto cmds = app.data;
936 
937     assert(cmds.length == 1);
938     cmds[0].directory.shouldEqual(dummy_dir);
939     cmds[0].file.shouldEqual("file1.cpp");
940     cmds[0].absoluteFile.shouldEqual(dummy_dir ~ "/file1.cpp");
941 }
942 
943 @("Should be a DB read from a relative path with the contained paths adjusted appropriately")
944 unittest {
945     auto app = appender!(CompileCommand[])();
946     raw_dummy3.parseCommands(CompileDbFile("path/compile_db.json"), app);
947     auto cmds = CompileCommandDB(app.data);
948 
949     // trusted: constructing a path in memory which is never used for writing.
950     auto abs_path = () @trusted{ return getcwd() ~ "/path"; }();
951 
952     auto found = cmds.find(abs_path ~ "/dir2/file3.cpp");
953     assert(found.length == 1);
954 
955     found.toString.shouldEqual(format("%s/dir2
956   file3.cpp
957   %s/dir2/file3.cpp
958   g++ -Idir1 -c -o binary file3.cpp
959 ", abs_path, abs_path));
960 }
961 
962 @("shall extract arguments, file, directory and output with absolute paths")
963 unittest {
964     auto app = appender!(CompileCommand[])();
965     raw_dummy4.parseCommands(CompileDbFile("path/compile_db.json"), app);
966     auto cmds = CompileCommandDB(app.data);
967 
968     // trusted: constructing a path in memory which is never used for writing.
969     auto abs_path = () @trusted{ return getcwd() ~ "/path"; }();
970 
971     auto found = cmds.find(buildPath(abs_path, "dir2", "file3.cpp"));
972     assert(found.length == 1);
973 
974     found.toString.shouldEqual(format("%s/dir2
975   file3.cpp
976   %s/dir2/file3.cpp
977   file3.o
978   %s/dir2/file3.o
979   -Idir1 -c -o binary file3.cpp
980 ", abs_path, abs_path, abs_path));
981 }
982 
983 @("shall be the compiler flags derived from the arguments attribute")
984 unittest {
985     auto app = appender!(CompileCommand[])();
986     raw_dummy4.parseCommands(CompileDbFile("path/compile_db.json"), app);
987     auto cmds = CompileCommandDB(app.data);
988 
989     // trusted: constructing a path in memory which is never used for writing.
990     auto abs_path = () @trusted{ return getcwd() ~ "/path"; }();
991 
992     auto found = cmds.find(buildPath(abs_path, "dir2", "file3.cpp"));
993     assert(found.length == 1);
994 
995     found[0].parseFlag(defaultCompilerFilter).flags.shouldEqual(["-I",
996             buildPath(abs_path, "dir2", "dir1")]);
997 }
998 
999 @("shall find the entry based on an output match")
1000 unittest {
1001     auto app = appender!(CompileCommand[])();
1002     raw_dummy4.parseCommands(CompileDbFile("path/compile_db.json"), app);
1003     auto cmds = CompileCommandDB(app.data);
1004 
1005     // trusted: constructing a path in memory which is never used for writing.
1006     auto abs_path = () @trusted{ return getcwd() ~ "/path"; }();
1007 
1008     auto found = cmds.find(buildPath(abs_path, "dir2", "file3.o"));
1009     assert(found.length == 1);
1010 
1011     found[0].absoluteFile.shouldEqual(buildPath(abs_path, "dir2", "file3.cpp"));
1012 }
1013 
1014 @("shall parse the compilation database when *arguments* is a json list")
1015 unittest {
1016     auto app = appender!(CompileCommand[])();
1017     raw_dummy5.parseCommands(CompileDbFile("path/compile_db.json"), app);
1018     auto cmds = CompileCommandDB(app.data);
1019 
1020     // trusted: constructing a path in memory which is never used for writing.
1021     auto abs_path = () @trusted{ return getcwd() ~ "/path"; }();
1022 
1023     auto found = cmds.find(buildPath(abs_path, "dir2", "file3.o"));
1024     assert(found.length == 1);
1025 
1026     found[0].absoluteFile.shouldEqual(buildPath(abs_path, "dir2", "file3.cpp"));
1027 }
1028 
1029 @("shall parse the compilation database and find a match via the glob pattern")
1030 unittest {
1031     import std.path : baseName;
1032 
1033     auto app = appender!(CompileCommand[])();
1034     raw_dummy5.parseCommands(CompileDbFile("path/compile_db.json"), app);
1035     auto cmds = CompileCommandDB(app.data);
1036 
1037     auto found = cmds.find("*/dir2/file3.cpp");
1038     assert(found.length == 1);
1039 
1040     found[0].absoluteFile.baseName.shouldEqual("file3.cpp");
1041 }